JEP 495: Simple Source Files and Instance Main Methods (Fourth Preview)

AuthorsRon Pressler, Jim Laskey, & Gavin Bierman
OwnerGavin Bierman
TypeFeature
ScopeSE
StatusCompleted
Release24
Componentspecification / language
Discussionamber dash dev at openjdk dot org
Relates toJEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)
JEP 494: Module Import Declarations (Second Preview)
Reviewed byAlex Buckley, Brian Goetz
Endorsed byBrian Goetz
Created2024/07/09 09:47
Updated2024/11/19 21:32
Issue8335984

Summary

Evolve the Java programming language so that beginners can write their first programs without needing to understand language features designed for large programs. Far from using a separate dialect of the language, beginners can write streamlined declarations for single-class programs and then seamlessly expand their programs to use more advanced features as their skills grow. Experienced developers can likewise enjoy writing small programs succinctly, without the need for constructs intended for programming in the large. This is a preview language feature.

History

This feature was first proposed for preview by JEP 445 (JDK 21) and subsequently improved and refined by JEP 463 (JDK 22) and JEP 477 (JDK 23). We here propose to preview it for a fourth time, with new terminology and a revised title but otherwise unchanged, in order to gain additional experience and feedback.

Goals

Motivation

The Java programming language excels for large, complex applications developed and maintained over many years by large teams. It has rich features for data hiding, reuse, access control, namespace management, and modularity which allow components to be cleanly composed while being developed and maintained independently. With these features, components can expose well-defined interfaces for their interaction with other components while hiding internal implementation details so as to permit the independent evolution of each. Indeed, the object-oriented paradigm itself is designed for plugging together pieces that interact through well-defined protocols and abstract away implementation details. This composition of large components is called programming in the large.

The Java programming language is also, however, intended to be a first language. When programmers first start out they do not write large programs, in a team — they write small programs, alone. They have no need for encapsulation and namespaces, useful to separately evolve components written by different people. When teaching programming, instructors start with the basic programming in the small concepts of variables, control flow, and subroutines. At that stage there is no need for the programming-in-the-large concepts of classes, packages, and modules. Making the language more welcoming to newcomers is in the interest of Java veterans but they, too, may enjoy writing small programs more concisely, without any programming-in-the-large constructs.

Consider the classic Hello, World! example that is often a beginner's first program:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

There is too much clutter here — too much code, too many concepts, and too many constructs — for what the program does.

The new programmer encounters these concepts at the worst possible time, before they learn about variables and control flow, and when they cannot appreciate the utility of programming-in-the-large constructs for keeping a large program well organized. Instructors often offer the admonition, "don't worry about that, you'll understand it later." This is unsatisfying to they and their students alike, and leaves students with the enduring impression that the language is complicated.

The motivation for this work is not merely to reduce ceremony. We aim to help programmers that are new to the Java language, or to programming in general, learn the language in a manner that introduces concepts in the right order: Start with the fundamental programming-in-the-small concepts, such as doing simple textual I/O and processing arrays with for loops, and proceed to advanced programming-in-the-large concepts only when they are actually beneficial and can be more easily grasped.

The motivation for this work is, moreover, not only to help beginning programmers. We aim to help everyone who writes small programs, whether they be students, system administrators writing command-line utilities, or domain experts prototyping core algorithms that will eventually be used in the heart of an enterprise-scale software system.

We propose to make it easier to write small programs not by changing the structure of the Java language — code is still enclosed in methods, which are enclosed in classes, which are enclosed in packages, which are enclosed in modules — but by hiding such details until they are useful.

Description

First, we allow main methods to omit the infamous boilerplate of public static void main(String[] args), which simplifies the Hello, World! program to:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Second, we introduce a simple form of source file that lets developers get straight to the code, without a superfluous class declaration:

void main() {
    System.out.println("Hello, World!");
}

Third, in simple source files we automatically import a few useful methods for console input and output, thereby avoiding the mysterious System.out.println:

void main() {
    println("Hello, World!");
}

Finally, for programs that go beyond Hello, World! and need, for example, basic data structures or file I/O, in simple source files we automatically import a range of standard APIs beyond just the java.lang package.

These changes combine to offer an on-ramp, that is, a gradual incline that merges gracefully onto the highway. As beginners move on to larger programs, they need not discard what they learned in the early stages, but, rather, they see how it all fits within the larger picture. As experienced developers proceed from prototype to production, they can smoothly grow their code into components of larger programs.

This is a preview language feature, disabled by default

To try the examples below in JDK 24 you must enable preview features:

Instance main methods

In order to write and run programs, beginners will learn about the entry point of a program. The Java Language Specification (JLS) explains that the entry point of a Java program is a method called main:

The Java Virtual Machine starts execution by invoking the method main of some specified class or interface, passing it a single argument which is an array of strings.

The JLS further states:

The method main must be declared public, static, and void. It must specify a formal parameter whose declared type is array of String.

These requirements on the declaration of main are historical and unnecessary. We can streamline the entry point of a Java program in two ways: Allow main to be non-static, and drop the requirements for public and an array parameter. These changes allow us to write Hello, World! with no public modifier, no static modifier, and no String[] parameter, postponing the introduction of those constructs until they are needed:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Assuming this program is in the file HelloWorld.java, we can run it directly with the source-code launcher:

$ java HelloWorld.java

The launcher compiles HelloWorld.java in memory, then finds and invokes a main method:

Any main method that can be invoked under this protocol is known as a launchable main method. For example, the HelloWorld class has one launchable main method, namely void main().

Simple source files

In the Java language, every class resides in a package and every package resides in a module. Modules and packages provide namespacing and encapsulation for classes, but small programs that consist of a few classes do not need these concepts. Accordingly, developers can omit package and module declarations, and their classes will reside in an unnamed package of an unnamed module.

Classes provide namespacing and encapsulation for fields and methods. We should not require beginners to understand these concepts before they are comfortable with the basic building blocks of variables, control flow, and subroutines. Accordingly, we can stop requiring class declarations for small programs that consist of a few fields and methods, just as we do not require package or module declarations.

Henceforth, if the Java compiler encounters a source file with fields and methods that are not enclosed in a class declaration, it will consider the source file to implicitly declare a class whose members are the unenclosed fields and methods. Such a source file is called a simple source file.

With this change, we can write Hello, World! as a simple source file:

void main() {
    System.out.println("Hello, World!");
}

The implicitly declared class of a simple source file

Since the fields and methods declared in a simple source file are interpreted as members of the implicitly declared class, we can write Hello, World! by calling a method declared nearby:

String greeting() { return "Hello, World!"; }

void main() {
    System.out.println(greeting());
}

or by accessing a field:

String greeting = "Hello, World!";

void main() {
    System.out.println(greeting);
}

Since a simple source file declares a class implicitly, the class does not have a name that can be used in code. We can refer to the current instance of the class via this, either explicitly or, as above, implicitly, but we cannot instantiate the class with new. This reflects an important tradeoff: If beginners have not yet learned object-oriented concepts such as classes, then writing code in a simple source file should not require a class declaration — which is what would give the class a name usable with new.

Assuming our simple source file is called HelloWorld.java, we can run it directly with the source-code launcher:

$ java HelloWorld.java

The launcher compiles HelloWorld.java in memory, treating its fields and methods as if they are members of a class called HelloWorld, deriving the name of the class from the name of the file. The launcher then finds and invokes a main method, as described earlier.

If a simple source file has a launchable main method that is an instance method, then running that file with the java launcher is equivalent to embedding it in an anonymous class declaration, instantiating the anonymous class, and invoking the launchable main method:

new Object() {

    String greeting = "Hello, World!";

    void main() {
        System.out.println(greeting);
    }

}.main();

The javadoc tool can generate documentation from a simple source file, even though the implicitly declared class cannot be referenced by other classes and thus cannot be used to define an API. Documenting the members of the implicitly declared class may be useful for beginners learning about javadoc, and for experienced developers prototyping code intended to be used in a larger program.

Interacting with the console

Many beginner programs need to interact with the console. Writing to the console ought to be a simple method invocation, but traditionally it requires using the qualified name System.out.println. This is mildly painful for experienced developers, but deeply mysterious to the beginner: What is System, what is out, and what are the dots for?

Even worse is reading from the console which, again, ought to be a simple method invocation. Since writing to the console involves System.out, it seems reasonable that reading would involve System.in, but getting a String from System.in requires all this code:

try {
    BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    String line = reader.readLine();
    ...
} catch (IOException ioe) {
    ...
}

Experienced developers are used to this boilerplate, but for the beginner this code contains yet more mysterious concepts, leading to a plethora of questions: What are try and catch, what is a BufferedReader, what is an InputStreamReader, and whatever is an IOException? There are other approaches, but none is significantly better, especially for the beginner.

To simplify the writing of interactive small programs, we make five methods available for use in simple source files:

public static void println(Object obj);
public static void println();
public static void print(Object obj);
public static String readln(String prompt);
public static String readln();

A beginner can now write Hello, World! as:

void main() {
    println("Hello, World!");
}

They can then easily move on to the simplest of interactive programs:

void main() {
    String name = readln("Please enter your name: ");
    print("Pleased to meet you, ");
    println(name);
}

The five static methods above are declared in the new class java.io.IO, which is a preview API in JDK 24. Every simple source file imports these static methods automatically, as if the declaration

import static java.io.IO.*;

appears at the start of every simple source file.

Automatic import of the java.base module

Many other classes in the Java Platform API are useful in small programs. They can be imported explicitly at the start of a simple source file:

import java.util.List;

void main() {
    var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
    for (var name : authors) {
        println(name + ": " + name.length());
    }
}

Experienced developers will find this natural, though for convenience some might be inclined to use import-on-demand declarations (i.e., import java.util.*). For beginners, however, any form of import is another source of mystery, requiring an understanding of the package hierarchy of the Java API.

To further simplify the writing of small programs, we make all of the public top level classes and interfaces of the packages exported by the java.base module available for use in simple source files, as if they were imported on demand. Popular APIs in commonly used packages such as java.io, java.math, and java.util are thus immediately usable. In the example above, import java.util.List can be removed since List will be imported automatically.

A companion JEP proposes a new import declaration, import module M, which imports, on demand, all of the public top level class and interfaces of the packages exported by module M. Every simple source file is considered to import the java.base module automatically, as if the declaration

import module java.base;

appears at the start of every simple source file.

Growing a program

A small program in a simple source file is focused on what the program does, omitting concepts and constructs it does not need. Even so, all members are interpreted as in an ordinary class. To evolve a simple source file into a ordinary source file, all we need to do is wrap its fields and methods in an explicit class declaration, and add the automatic imports. For example, this simple source file:

void main() {
    var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
    for (var name : authors) {
        println(name + ": " + name.length());
    }
}

can be evolved into an ordinary source file that declares a single class:

import static java.io.IO.*;
import module java.base;

class NameLengths {
    void main() {
        var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
        for (var name : authors) {
            println(name + ": " + name.length());
        }
    }
}

The main method does not change in any way. Thus turning a small program into a class that can serve as a component in a larger program is always straightforward.

Alternatives